Skip to content

Replace the web UI's sign-in form with auth-gate states - #23

Merged
16francej merged 1 commit into
mainfrom
web-ui-auth-gate-states
Jul 30, 2026
Merged

Replace the web UI's sign-in form with auth-gate states#23
16francej merged 1 commit into
mainfrom
web-ui-auth-gate-states

Conversation

@16francej

@16francej 16francej commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

web-ui never authenticates anyone — the portal does, over OIDC — yet the SPA rendered a credential form for every 401.

In production that form could not sign anyone in. resolveIdentity ignores the webuiuser cookie once CORE_SIGNING_SECRET is set, so Continue POSTed to the portal, took a 401, and api() surfaced the server's raw string as the error text. The user saw the words "sign in" in red, with no way forward. The actual remedy was to reload, which nothing told them.

What changes

A surface that cannot authenticate anyone should render a state with one action, not a form — which is what the admin surface already does (plugins/admin/public/index.html:3776). And the server, not the client, decides which state applies.

  • mode: "portal" | "dev" rides on the 401 and /me bodies, derived from one COOKIE_AUTH const that resolveIdentity itself now reads. Prod can no longer render the dev form.
  • Portal — "Your session ended", one button to /auth/login carrying returnTo so the user lands back where they were. No field.
  • Unreachable — "We couldn't reach the assistant" + Try again. This state did not exist: main.ts ended in a bare void boot() and boot() only special-cased exactly 401, so a 502 or a dropped connection was a blank page with a console error.
  • Dev — keeps the field, but says why it exists and names the env var that turns it off. A DEV chip plus a persistent ochre top banner mark the instance as unauthenticated for as long as the session lasts, since the reason outlives the sign-in.

Net effect is less code: the form, its submit handler, and the POST /signin round-trip survive only on the dev path.

Adjacent fixes

Each of these would have left the change incoherent if skipped:

  • Sign out in portal mode now calls /auth/logout. It was clearing a cookie the server ignores — a no-op in production, which would have looked especially broken next to a new "Your session ended" screen.
  • POST /signin 404s outside cookie-auth mode instead of setting a cookie that gets ignored.
  • A rejected principal gets a message naming WEB_UI_PRINCIPALS instead of not allowed.
  • The boot warning about WEB_UI_PRINCIPALS no longer fires in portal mode, where no principal can cookie-sign-in at all.
  • /signout added to vite's proxy list — a pre-existing gap found while testing; sign-out was silently broken on the standalone-vite dev path.
  • .impersonation-banner generalises to .top-banner / .layout.bannered, now that the dev banner is a second caller.

Form mechanics, while in here: a real <label> instead of placeholder-as-label, type="email" with autocomplete="username" so password managers stop offering a password fill, role="alert" on the error, and a pending guard against double submits.

Screenshots

All five states captured against a running instance, with the env flipped to produce each one:

https://claude.ai/code/artifact/73ed970f-5f4c-4813-90c3-919eeb6b530c

Verification

Exercised live on localhost:8096 (real server, embedded vite): sign in → banner → sign out → gate; the 403 rejected-principal error; mode flipping to portal under CORE_SIGNING_SECRET; /signin 404ing outside dev; and Try again recovering once the server came back.

430 web-ui tests pass. Repo typecheck, both web-ui tsconfigs, and eslint clean.

🤖 Generated with Claude Code


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

web-ui never authenticates anyone — the portal does, over OIDC — yet the SPA
rendered a credential form for every 401. In production that form could not
sign anyone in: resolveIdentity ignores the webuiuser cookie once
CORE_SIGNING_SECRET is set, so Continue POSTed to the portal, took a 401, and
api() surfaced the server's raw string as the error. The user saw the words
"sign in" in red with no way forward.

The surface now renders a state with one action instead of a form, and the
server says which state applies rather than the client inferring it:

- The 401 and /me bodies carry mode: "portal" | "dev", derived from the single
  COOKIE_AUTH const that resolveIdentity itself now reads. Prod can no longer
  render the dev form.
- Portal mode: "Your session ended" with one button to /auth/login, carrying
  returnTo so the user lands back where they were. No field.
- Unreachable: "We couldn't reach the assistant" with Try again. This state did
  not exist — main.ts ended in a bare void boot() and boot() only special-cased
  exactly 401, so a 502 or a dropped connection was a blank page.
- Dev mode: keeps the field, but says why it exists and names the env var that
  turns it off. A DEV chip and a persistent ochre top banner mark the instance
  as unauthenticated for as long as the session lasts.

POST /signin now 404s outside cookie-auth mode, and a rejected principal gets a
message naming WEB_UI_PRINCIPALS instead of "not allowed". Sign out in portal
mode ends the portal session rather than clearing a cookie the server ignores.

The impersonation banner generalises to .top-banner / .layout.bannered, since
the dev banner is the second caller. Form mechanics: a real label instead of
placeholder-as-label, type=email with autocomplete=username so password
managers stop offering a password fill, role=alert on the error, and a
pending guard against double submits.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@16francej
16francej merged commit f23b365 into main Jul 30, 2026
15 checks passed
@16francej
16francej deleted the web-ui-auth-gate-states branch July 30, 2026 06:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant